package db

import (
	"context"
	"fmt"
	"runtime/debug"
	"time"

	"v8s-watcher/internal"
	"v8s-watcher/internal/configuration"
	"v8s/logger"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"

	"go.uber.org/zap"
)

type (
	ProjectsRepository struct {
		log *zap.SugaredLogger

		client     *MongoClient
		properties configuration.MongoProperties
	}

	ProjectResult struct {
		Project *Project
		Error   error
	}

	Project struct {
		DocumentID primitive.ObjectID `bson:"_id,omitempty"`
		ID         string             `bson:"project-id"`
		Tasks      []*Task            `bson:"tasks"`
	}

	Task struct {
		ID       internal.TaskID                `bson:"task-id"`
		Status   TaskStatus                     `bson:"status"`
		Compose  Compose                        `bson:"compose"`
		Clusters map[ClusterName]ClusterDetails `bson:"clusters"`
	}
	ClusterName    string
	ClusterDetails struct {
		Status    ClusterStatus               `bson:"status"`
		Resources []Resource                  `bson:"resources"`
		Services  map[ServiceName]ServiceInfo `bson:"services"`
	}
	Resource struct {
		Kind      string         `bson:"kind"`
		Name      string         `bson:"name"`
		BelongsTo string         `bson:"belongs-to"`
		Status    ResourceStatus `bson:"status"`
		//TODO:
		//добавить получение из кубера StatusDetails string
	}
	ServiceName    string
	ResourceStatus string
	Compose        struct {
		Services map[string]interface{} `bson:"services"`
	}

	ServiceInfo struct {
		Endpoints []string `bson:"endpoints,omitempty"`
	}

	TaskStatus    string
	ClusterStatus string
)

const (
	ProjectsCollection = "projects"

	ResourceStatusPending   ResourceStatus = "Pending"
	ResourceStatusError     ResourceStatus = "Error"
	ResourceStatusRunning   ResourceStatus = "Running"
	ResourceStatusFailed    ResourceStatus = "Failed"
	ResourceStatusUnknown   ResourceStatus = "Unknown"
	ResourceStatusSucceeded ResourceStatus = "Succeeded"
	ResourceStatusBound     ResourceStatus = "Bound"
	ResourceStatusLost      ResourceStatus = "Lost"

	ClusterStatusStarting ClusterStatus = "starting"
	ClusterStatusRunning  ClusterStatus = "running"
	ClusterStatusStopped  ClusterStatus = "stopped"
	ClusterStatusStopping ClusterStatus = "stopping"
	ClusterStatusError    ClusterStatus = "error"
	ClusterStatusDeleting ClusterStatus = "deleting"

	CreationRequestTaskStatus   TaskStatus = "creation-request"
	RunningTaskStatus           TaskStatus = "running"
	DeletionRequestTaskStatus   TaskStatus = "deletion-request"
	DeletedTaskStatus           TaskStatus = "deleted"
	SuspensionRequestTaskStatus TaskStatus = "suspension-request"
	SuspendedTaskStatus         TaskStatus = "suspended"
	ResumeRequestTaskStatus     TaskStatus = "resume-request"
	UpdateRequestTaskStatus     TaskStatus = "update-request"
	ErrorTaskStatus             TaskStatus = "error"

	lockTTL = time.Second * 10
	seenTTL = time.Second * 10

	seenAtProperty   = "watcher-seen-at"
	lockedAtProperty = "watcher-locked-at"
)

func CreateProjectsRepository(client *MongoClient, properties configuration.MongoProperties) *ProjectsRepository {
	repository := &ProjectsRepository{
		log: logger.Logger().Named("db-projects-repository").Sugar(),

		client:     client,
		properties: properties,
	}

	err := repository.init()
	if err != nil {
		panic(fmt.Errorf("failed to init repository: %w", err))
	}

	return repository
}

func (repository *ProjectsRepository) init() error {
	//names, err := repository.client.
	//	Database(repository.properties.Database).
	//	Collection(projectsCollection).
	//	Indexes().
	//	CreateMany(context.TODO(), []mongo.IndexModel{
	//		mongo.IndexModel{
	//			Keys:    bson.D{},
	//			Options: nil,
	//		},
	//	})
	//
	//return err
	return nil
}

func (repository *ProjectsRepository) ListProjectsSince(ctx context.Context, point time.Time, limit int) <-chan ProjectResult {
	notRunningAndNotHandled := and(
		bson.M{"tasks.status": bson.M{"$ne": RunningTaskStatus}},
		or(
			bson.M{"tasks.clusters": bson.M{"$ne": bson.M{}}},
		),
	)

	running := bson.M{"tasks.status": RunningTaskStatus}

	notSeen := or(
		bson.M{seenAtProperty: bson.M{"$exists": false}},
		bson.M{seenAtProperty: bson.M{"$lt": primitive.NewDateTimeFromTime(point.Add(-seenTTL))}})

	return repository.createChannel(limit, func() *mongo.SingleResult {
		lockFree := or(
			bson.M{lockedAtProperty: bson.M{"$exists": false}},
			bson.M{lockedAtProperty: bson.M{"$lt": primitive.NewDateTimeFromTime(time.Now().Add(-lockTTL))}})

		where := and(lockFree, notSeen, or(notRunningAndNotHandled, running))
		update := set(bson.M{lockedAtProperty: primitive.NewDateTimeFromTime(time.Now())})

		return repository.collection().FindOneAndUpdate(ctx, where, update)
	})
}

func (repository *ProjectsRepository) SaveProject(ctx context.Context, project *Project) error {
	arrayFilters := make([]interface{}, 0, len(project.Tasks))
	updates := make([]bson.M, 0, len(project.Tasks)*2)

	for i, task := range project.Tasks {
		// Обходим все задачи, записываем новые значения Clusters
		// Чтобы на монге сформировать нормально запрос - используем array-filter
		current := task
		// filter - название фильтра, для такого айдишника задачи
		filter := fmt.Sprintf("task%d", i)

		arrayFilters = append(arrayFilters, bson.M{fmt.Sprintf("%s.task-id", filter): current.ID})

		updates = append(updates, bson.M{fmt.Sprintf("tasks.$[%s].clusters", filter): current.Clusters})
	}

	updates = append(updates, bson.M{seenAtProperty: primitive.NewDateTimeFromTime(time.Now())})

	update := merge(set(updates...), unset(bson.M{lockedAtProperty: ""}))
	filters := options.Update().SetArrayFilters(options.ArrayFilters{Filters: arrayFilters})

	return repository.updateByID(ctx, project, update, filters)
}

func (repository *ProjectsRepository) updateByID(ctx context.Context, project *Project, update interface{}, opts ...*options.UpdateOptions) error {
	result, err := repository.collection().UpdateByID(ctx, project.DocumentID, update, opts...)
	if err != nil {
		return fmt.Errorf("failed to save updated document '%s': %w", project.ID, err)
	}

	if result.MatchedCount < 1 {
		return fmt.Errorf("none of documents match id '%s'", project.ID)
	}

	return nil
}

func (repository *ProjectsRepository) createChannel(limit int, block func() *mongo.SingleResult) chan ProjectResult {
	channel := make(chan ProjectResult, limit)

	go func() {
		defer func() {
			recovered := recover()
			if recovered != nil {
				debug.PrintStack()
				repository.log.Errorf("error happend while obtaining projects: %s", recovered)
			}

			close(channel)
		}()

		for {
			result := block()

			if result.Err() != nil && result.Err() != mongo.ErrNoDocuments {
				repository.log.Errorf("error happend while retrieving document: %s", result.Err())
				channel <- ProjectResult{Error: result.Err()}
				break
			}

			if result.Err() == mongo.ErrNoDocuments {
				break
			}

			project, err := convert(result)
			if err != nil {
				repository.log.Errorf("error happend while converting document: %s", err)
				channel <- ProjectResult{Error: result.Err()}
				break
			}

			channel <- ProjectResult{Project: project}
		}
	}()

	return channel
}

func (repository *ProjectsRepository) collection() *mongo.Collection {
	return repository.client.Database(repository.properties.Database).Collection(ProjectsCollection)
}

func convert(result *mongo.SingleResult) (*Project, error) {
	if result.Err() != nil {
		return nil, fmt.Errorf("failed to find project: %w", result.Err())
	}

	project := &Project{}
	if err := result.Decode(project); err != nil {
		return nil, fmt.Errorf("failed to decode project: %w", err)
	}

	return project, nil
}

func merge(items ...bson.M) bson.M {
	result := bson.M{}

	for _, item := range items {
		for property, value := range item {
			if result[property] != nil {
				panic(fmt.Sprintf("failed to merge operations, '%s' is duplicated", property))
			}

			result[property] = value
		}
	}

	return result
}

func unset(items ...bson.M) bson.M {
	return bson.M{"$unset": merge(items...)}
}

func set(items ...bson.M) bson.M {
	return bson.M{"$set": merge(items...)}
}

func or(items ...bson.M) bson.M {
	result := bson.A{}

	for _, item := range items {
		result = append(result, item)
	}

	return bson.M{"$or": result}
}

func and(items ...bson.M) bson.M {
	result := bson.A{}

	for _, item := range items {
		result = append(result, item)
	}

	return bson.M{"$and": result}
}
